page.tsx 4.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115
  1. 'use client';
  2. import { use, useEffect, useRef, useState } from 'react';
  3. import { useDonationHub } from '@/hooks/useDonationHub';
  4. import { fetchApi } from '@/lib/utils/client';
  5. import './style.scss';
  6. type Props = {
  7. params: Promise<{ widgetToken: string }>;
  8. searchParams: Promise<{ [key: string]: string|string[]|undefined }>;
  9. };
  10. type RankListResponse = {
  11. list: { rank: number; sponsorMemberID: number; sponsorName: string; totalAmount: number; donationCount: number }[];
  12. };
  13. type RankStyleConfig = {
  14. id: number;
  15. title: string;
  16. theme: number;
  17. period: number;
  18. isShowAmount: boolean;
  19. maxRankCount: number;
  20. titleFontFamily: string|null;
  21. titleFontSizePx: number;
  22. titleFontColor: string;
  23. rank1FontFamily: string|null;
  24. rank1FontSizePx: number;
  25. rank1FontColor: string;
  26. rank2FontFamily: string|null;
  27. rank2FontSizePx: number;
  28. rank2FontColor: string;
  29. rank3FontFamily: string|null;
  30. rank3FontSizePx: number;
  31. rank3FontColor: string;
  32. isShowDonationCount: boolean;
  33. isActive: boolean;
  34. };
  35. export default function RankPage({ params, searchParams }: Props) {
  36. const { widgetToken } = use(params);
  37. const sp = use(searchParams);
  38. const configID = typeof sp.configID === 'string' ? parseInt(sp.configID, 10) : null;
  39. const hubUrl = process.env.NEXT_PUBLIC_API_URL + '/hubs/donation';
  40. const { ranking, setRanking } = useDonationHub(widgetToken, hubUrl);
  41. const [styleCfg, setStyleCfg] = useState<RankStyleConfig|null>(null);
  42. const widgetRef = useRef<HTMLDivElement>(null);
  43. // 스타일 config 로드
  44. useEffect(() => {
  45. const qs = configID ? `?configID=${configID}` : '';
  46. fetchApi<RankStyleConfig>(`/api/widget/rank/config/${widgetToken}${qs}`, { silent: true }).then(res => {
  47. if (res.data) {
  48. setStyleCfg(res.data);
  49. }
  50. }).catch(() => {});
  51. }, [widgetToken, configID]);
  52. // 순위 데이터 로드
  53. useEffect(() => {
  54. const qs = configID ? `?configID=${configID}` : '';
  55. fetchApi<RankListResponse>(`/api/widget/rank/by-token/${widgetToken}${qs}`, { silent: true }).then(res => {
  56. if (res.data?.list) {
  57. setRanking(res.data.list);
  58. }
  59. }).catch(() => {});
  60. }, [widgetToken, configID, setRanking]);
  61. // CSS variables 적용
  62. useEffect(() => {
  63. if (!widgetRef.current) {
  64. return;
  65. }
  66. const el = widgetRef.current;
  67. el.style.setProperty('--rank-title-font-family', styleCfg?.titleFontFamily ?? 'inherit');
  68. el.style.setProperty('--rank-title-font-size', `${styleCfg?.titleFontSizePx ?? 18}px`);
  69. el.style.setProperty('--rank-title-color', styleCfg?.titleFontColor ?? '#FFFFFF');
  70. el.style.setProperty('--rank-1-font-family', styleCfg?.rank1FontFamily ?? 'inherit');
  71. el.style.setProperty('--rank-1-font-size', `${styleCfg?.rank1FontSizePx ?? 15}px`);
  72. el.style.setProperty('--rank-1-color', styleCfg?.rank1FontColor ?? '#FFD700');
  73. el.style.setProperty('--rank-2-font-family', styleCfg?.rank2FontFamily ?? 'inherit');
  74. el.style.setProperty('--rank-2-font-size', `${styleCfg?.rank2FontSizePx ?? 15}px`);
  75. el.style.setProperty('--rank-2-color', styleCfg?.rank2FontColor ?? '#C0C0C0');
  76. el.style.setProperty('--rank-3-font-family', styleCfg?.rank3FontFamily ?? 'inherit');
  77. el.style.setProperty('--rank-3-font-size', `${styleCfg?.rank3FontSizePx ?? 15}px`);
  78. el.style.setProperty('--rank-3-color', styleCfg?.rank3FontColor ?? '#CD7F32');
  79. }, [styleCfg]);
  80. const title = styleCfg?.title ?? '후원 순위';
  81. const isShowAmount = styleCfg?.isShowAmount ?? true;
  82. const isShowCount = styleCfg?.isShowDonationCount ?? false;
  83. return (
  84. <div ref={widgetRef} className="rank-widget">
  85. <div className="rank-title">{title}</div>
  86. <div className="rank-list">
  87. {ranking.map(item => {
  88. const badgeClass = item.rank <= 3 ? `badge-${item.rank}` : 'badge-default';
  89. return (
  90. <div key={`${item.rank}-${item.sponsorMemberID}`} className={`rank-item rank-${item.rank}`}>
  91. <div className={`rank-badge ${badgeClass}`}>{item.rank}</div>
  92. <div className="rank-name">{item.sponsorName}</div>
  93. {isShowAmount && (
  94. <div className="rank-amount">
  95. {item.totalAmount.toLocaleString()}원
  96. {isShowCount && ` (${item.donationCount}회)`}
  97. </div>
  98. )}
  99. </div>
  100. );
  101. })}
  102. </div>
  103. </div>
  104. );
  105. }